#!python

from gevent import monkey
monkey.patch_all()

from Crypto.Cipher import ARC4
from urllib2 import HTTPError
from email.utils import formatdate, parsedate
from gevent.pywsgi import WSGIServer
import mimetypes
import pymysql
from gevent import spawn
import os
import hashlib
import baidu_pcs_op2 as baidu_pcs_op
import shutil
import base64
import ConfigParser
import time
import traceback
import glob
import struct
import sys
from pprint import pprint
import sqlite3

DB_HOST = "www.simicloud.com"
#db_host = "114.215.159.201"
DB_USER="video"
DB_PW= "MuM5Z9rbdc4MrUeT"
DB_NAME = "video"

SERVER = "iws/1.0"

METHODS_ALLOW= ["GET", "HEAD"]

KEY="27Dvduoa9eQ1PHu1EfVGM36ZYMQ70eRk"
IDENTITY = 0xffeeddcc
EXTENSIONS=["*.mp4","*.m4a","*.rmvb","*.rm","*.3gp","*.mkv"]
EXTENSION_MAPS = {}
DEFAULT_TYPE = "text/plain"
EXT_MAPS={
    ".mp4":"video/mp4",
    ".rm":"video/rmvb",
    ".m4a":"video/m4a",
    ".rmvb":"video/rmvb",
    ".3gp":"video/3gp",
    ".mkv":"video/mkv"
}

def get_type(fn):
    base, ext = os.path.splitext(fn)
    t = EXTENSION_MAPS.get(ext.lower())
    if t:
        return t
    return DEFAULT_TYPE

def md5(t):
    m = hashlib.md5()
    m.update(t)
    return m.hexdigest()

def rc4(key, data):
    a=ARC4.new(key)
    return a.encrypt(data)

def encrypt(src,dst):
    if not os.path.exists(src):
        raise OSError("%s not exists")
    fout=open(dst,"wb")
    fin = open(src,"rb")
    fout.write(struct.pack("!L", IDENTITY))
    fn = os.path.basename(src).decode("gbk").encode("utf-8")
    fn_e = base64.b64encode(fn)
    fn_w = fn_e + (chr(0x00) * (1024 - len(fn_e)))
    fout.write(fn_w)
    fout.write(rc4(KEY,fin.read(4096)))

    remain = os.path.getsize(src) - 4096 - 4096
    while True:
        if remain <= 0:
            break
        block  = 4096
        if remain < 4096:
            block = remain

        data = fin.read(block)
        if not data:
            break
        fout.write(data)
        remain -= block

    fout.write(rc4(KEY,fin.read(4096)))
    fin.close()
    fout.close()

class Dbconn:
    def __init__(self, host, user, passwd, db):
        self.host = host
        self.user = user
        self.passwd = passwd
        self.db = db
        self.conn=None

    def connect(self):
        if not self.conn:
            self.conn = pymysql.connect(self.host,
                    self.user, self.passwd, self.db, charset="utf8", use_unicode=True)
        #self.conn.set_character_set("latin1")
        try:
            cur=self.conn.cursor()
            cur.execute("select 1+1 from dual where 0 > 1")
            cur.fetchall()
            cur.close()
            #self.conn.close()
        except:
            traceback.print_exc()
            try:
                self.conn.close()
            except:
                pass
            self.conn = pymysql.connect(self.host,
                    self.user, self.passwd, self.db, charset="utf8", use_unicode=True)
        return self.conn

class MovieSrv(object):
    def __init__(self):
        self.cache_init()
        self.cfg = ConfigParser.ConfigParser()
        self.cfg.read("config.ini")
        self.src_dir = os.path.normpath(self.cfg.get("server","download_directory"))
        self.dst_dir = os.path.normpath(self.cfg.get("server","www_root"))
        access_token = self.cfg.get("server","access_token")
        self.pcs_dir = '/apps/WiFiDisk/download'
        hostname = self.cfg.get("server", "domain_name")
        port = self.cfg.getint("server", "port")
        if port == 80:
            self.base_url = "http://%s/download" % (hostname,)
        else:
            self.base_url = 'http://%s:%d/download' % (hostname, port)

        self.db = Dbconn(DB_HOST, DB_USER, DB_PW, DB_NAME)

        self.pcs = baidu_pcs_op.PcsOp(access_token)

    def get_cur(self):
        conn = self.db.connect()
        cur = conn.cursor()
        return cur

    def cache_init(self):
        try:
            os.remove("cache.db")
        except:
            pass
        self.cache_conn = sqlite3.connect("cache.db")
        cur = self.cache_conn.cursor()
        cur.execute("PRAGMA page_size")
        rs = cur.fetchone()
        #page_size = 4096
        #page_total= 0
        #if rs:
        #    page_size = int(rs[0])
        #page_total = 8 * 1024 * 1024 / page_size
        #cur.execute("PRAGMA max_page_count = %d" % page_total)
        cur.execute("""
            create table video (
                origin_name text,
                current_name text,
                content_type text,
                content_length integer,
                etag text,
                last_modified text
            )
            """
        )
        self.cache_conn.commit()
        cur.close()

    def cache_fetch(self, fn):
        cur = self.cache_conn.cursor()
        cur.execute("select origin_name, current_name, content_type, content_length, etag, last_modified from video where \
current_name = '%s'" % fn)
        rs = cur.fetchone()
        cur.close()

        return rs

    def get_info_from_db(self, fn):
        rs = self.cache_fetch(fn)
        if rs:
            #print "from cache... %s" % (str(rs),)
            return rs
        cur = self.get_cur()
        cur.execute("select origin_name, current_name, content_type, content_length, etag, last_modified from video where \
current_name = '%s'" % fn)
        rs = cur.fetchone()
        cur.close()
        if rs:
            self.cache_store(rs)
        return rs

    def get_info_from_disk(self, fn):
        origin_name = ''
        current_name = os.path.basename(fn)
        content_type = get_type(fn)
        content_length = os.path.getsize(fn)
        last_modified = formatdate(timeval = os.path.getmtime(fn), usegmt = True)
        etag = md5(last_modified)

        retrun (origin_name, current_name, content_type, content_length, etag, last_modified)

    def update_head_times(self, fn):
        cur = self.get_cur()
        cur.execute("update video set head_times = head_times + 1 where current_name = '%s'" % fn)
        self.db.conn.commit()
        cur.close()

    def handle_head(self, env, start_response):
        p = env["PATH_INFO"]
        fname = p.replace("/download/", "", 1)
        full_fname = os.path.normpath(os.path.join(self.dst_dir, fname))
        d = os.path.normpath(self.dst_dir)
        if not full_fname.startswith(d):
            start_response("400 Bad Request", [("Content-Type","text/plain"), ("Server", SERVER)])
            return ["Bad Request"]

        info = self.get_info_from_db(fname)
        if not info:
            if os.path.exists(full_fname):
                info = get_info_from_disk(full_fname)
            else:
                start_response("404 Not Found",[("Content-Type","text/plain"), ("Server", SERVER)])
                return ["File not found"]

        headers=[]
        headers.append(("Content-Type", str(info[2])))
        headers.append(("Content-Length", str(info[3])))
        headers.append(("ETag",'"%s"' % str(info[4])))
        headers.append(("Last-Modified", str(info[5])))
        headers.append(("Server", SERVER))
        spawn(self.update_head_times, fname)
        start_response("200 OK", headers)
        return []

    def handle_get(self, env, start_response):
        p = env["PATH_INFO"]
        fname = p.replace("/download/", "", 1)
        full_fname = os.path.normpath(os.path.join(self.dst_dir, fname))

        if not full_fname.startswith(self.dst_dir):
            start_response("400 Bad Request", [("Content-Type","text/plain"), ("Server", SERVER)])
            return ["Bad Request"]

        if not os.path.exists(full_fname):
            start_response("404 Not Found", [("Content-Type", "text/plain"), ("Server", SERVER)])
            return ["File not found"]

        # check If-Modified-Since
        if_modified_since = env.get("HTTP_IF_MODIFIED_SINCE")
        if if_modified_since:
            d = int(time.mktime(parsedate(if_modified_since)))
            d1 = int(os.path.getmtime(full_fname) + time.timezone)
            if d >= d1:
                start_response("304 Not Modified", [("Server", SERVER)])
                return []

        # check ETag
        if_none_match = env.get("HTTP_IF_NONE_MATCH")
        if if_none_match:
            etag = if_none_match.replace('"','')
            etag_1 = md5(formatdate(timeval=os.path.getmtime(full_fname), usegmt = True))
            if etag == etag_1:
                start_response("304 Not Modified", [("Server", SERVER)])
                return []

        info = self.get_info_from_db(fname)
        if not info:
            info = self.get_info_from_disk(full_fname)

        http_range = env.get("HTTP_RANGE")

        headers=[]
        headers.append(("Content-Type", str(info[2])))

        headers.append(("ETag",'"%s"' % str(info[4])))
        headers.append(("Last-Modified", str(info[5])))
        headers.append(("Server", SERVER))

        if http_range:
            r = http_range.split("=")[1]
            if ',' in r:
                start_response("416 Range Not Satisfiable", [("Content-Type","text/plain"), ("Server", SERVER)])
                return ["Multiple range is not supported"]

            start,end = r.split("-")
            if start:
                s = int(start)
            else:
                s = 0
            if end:
                e = int(end)
            else:
                e = info[3] - 1
            if (e > (info[3] - 1)) or (s < 0):
                start_response("416 Range Not Satisfiable", [("Content-Type","text/plain"), ("Server", SERVER)])
                return ["The range is out of scope"]
            r1  = "%d-%d/%d" % (s, e, info[3])
            headers.append(("Content-Range", "bytes %s" % r1))
            headers.append(("Content-Length", "%d" % (e - s + 1)))
            wfile = start_response("206 Partial Content", headers)
            self.send_content_range(full_fname, wfile, s, e)
        else:
            headers.append(("Content-Length", str(info[3])))
            wfile = start_response("200 OK", headers)
            self.send_content_normal(full_fname, wfile)
        return []

    def send_content_range(self, fn, write_func, start, end):
        with open(fn, "rb") as fp_i:
            fp_i.seek(start)
            remain = end - start + 1
            block = 4096
            while True:
                if remain <= 0:
                    break
                if remain < 4096:
                    block = remain
                data = fp_i.read(block)
                if not data:
                    break
                write_func(data)
                remain -= block

    def send_content_normal(self, fn, write_func):
        with open(fn, "rb") as fp_in:
            while True:
                data = fp_in.read(4096)
                if not data:
                    break
                write_func(data)

    def application(self, env, start_response):
        p = env["PATH_INFO"]
        m = env["REQUEST_METHOD"]

        if m not in METHODS_ALLOW:
            start_response("400 Bad Request", [("Content-Type","text/plain"), ("Server", SERVER)])
            return ["Bad Request"]

        if p == "/" or p == "":
            start_response("200 OK", [("Content-Type","text/plain"), ("Server", SERVER)])
            return ["welcome to xxxx!"]

        if p.startswith("/download/"):
            if m == "HEAD":
                return self.handle_head(env, start_response)
            return self.handle_get(env, start_response)

        full_name = os.path.normpath("%s%s" % (self.dst_dir, p))
        if  not full_name.startswith(self.dst_dir):
            start_response("400 Bad Request", [("Content-Tyep", "text/plain"), ("Server", SERVER)])
            return ["Bad Request"]

        if not os.path.exists(full_name):
            start_response("200 OK", [("Content-Type", "text/plain"), ("Server", SERVER)])
            return ["The document you request that does not exists"]

        # check If-Modified-Since
        if_modified_since = env.get("HTTP_IF_MODIFIED_SINCE")
        if if_modified_since:
            d = int(time.mktime(parsedate(if_modified_since)))
            d1 = int(os.path.getmtime(full_name) + time.timezone)
            if d >= d1:
                start_response("304 Not Modified", [("Server", SERVER)])
                return []

        # check ETag
        etag_1 = md5(formatdate(timeval=os.path.getmtime(full_name), usegmt = True))
        if_none_match = env.get("HTTP_IF_NONE_MATCH")
        if if_none_match:
            etag_2 = if_none_match.replace('"','')
            if etag_2 == etag_1:
                start_response("304 Not Modified", [("Server", SERVER)])
                return []

        wfile = start_response("200 OK", [
                ("Content-Type", get_type(full_name)),
                ("Content-Length", os.path.getsize(full_name)),
                ("Last-Modified", formatdate(timeval=os.path.getmtime(full_name), usegmt=True)),
                ("ETag", '"%s"' % etag_1),
                ("Server", SERVER),
            ])

        if m == "GET":
            self.send_content_normal(full_name, wfile)
        return []

    def gen_info(self, fn):
        mtime = os.path.getmtime(fn)
        last_modified = formatdate(timeval = mtime,usegmt = True)
        content_type = get_type(fn)
        content_length = os.path.getsize(fn) + 1024 + 4

        etag = md5(last_modified)
        current_name = md5(os.path.basename(fn)).upper() + ".iie"
        origin_name = os.path.basename(fn).decode("gbk").encode("utf-8")
        return (origin_name, current_name, content_type, content_length, etag, last_modified)

    def save_info(self, info):
        cur = self.get_cur()

        # check exists
        cur.execute("select current_name from video where current_name = '%s'" % info[1])
        rs = cur.fetchall()
        if rs:
            cur.close()
            return

        cur.execute("""insert into video (origin_name, current_name, content_type, content_length, etag, last_modified, download_time) \
values('%s', '%s', '%s', %d, '%s', '%s', '%s')
""" % (pymysql.escape_string(info[0]), info[1],info[2],info[3],info[4],info[5], time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        )
        )
        self.db.conn.commit()
        cur.close()
        #self.cache_store(info)

    def cache_store(self, info):
        #print "cache... %s" % (str(info),)
        cur = self.cache_conn.cursor()
        cur.execute("""insert into video(origin_name, current_name, content_type, content_length, etag, last_modified)
                values('{origin_name}','{current_name}','{content_type}',{content_length},'{etag}','{last_modified}')""".format(**dict(
                    origin_name = info[0].encode("utf-8"), current_name = str(info[1]), content_type = str(info[2]),
                    content_length = str(info[3]), etag=str(info[4]),
                    last_modified = str(info[5])
                    )
                  )
                )
        self.cache_conn.commit()
        cur.close()

    def encrypt_file(self):
        while True:
            try:
                for ext in EXTENSIONS:
                    sources = glob.glob(os.path.join(self.src_dir, ext))
                    for f in sources:
                        info = self.gen_info(f)
                        self.save_info(info)

                        dst = os.path.normpath(os.path.join(self.dst_dir, info[1]))
                        #ext = os.path.splitext(f)
                        encrypt(f,dst)

                        shutil.move(f,f+"_del")
                        LOG("new file %s encrypt successed." % f)
            except:
                traceback.print_exc()

            time.sleep(20)

    def upload_to_pcs(self):
        #exts = [e.replace("*",'') for e in EXTENSIONS]
        exts = [".iie"]
        while True:
            try:
                tasks = self.pcs.list_task()
                self.print_task_info(tasks)

                pendding = []

                if self.cancel_timeout_task(tasks):
                    time.sleep(5)
                    tasks = self.pcs.list_task()

                if len(tasks) == 5:
                    time.sleep(20)
                    continue

                for f in os.listdir(self.dst_dir):
                    # is file?
                    if not os.path.isfile(os.path.join(self.dst_dir, f)):
                        continue

                    if f.endswith("_del"):
                        continue

                    # extension match?
                    _,ext = os.path.splitext(f)
                    if ext not in exts:
                        continue

                    rname = "%s/%s" % (self.pcs_dir,  f)
                    try:
                        self.pcs.info(rname)
                        self.upload_finished(f)
                        oname="%s/%s" % (self.dst_dir, f)
                        nname = "%s/%s_del" % (self.dst_dir, f)
                        shutil.move(oname, nname)

                        LOG("%s upload successed." % f)
                        continue
                    except HTTPError:
                        pass
                    except:
                        traceback.print_exc()

                    if not self.task_exists(tasks, f):
                        pendding.append(f)
                    else:
                        LOG("%s is download..." % f)

                for i in range(0, 5-len(tasks)):
                    if len(pendding) > i:
                        u = "%s/%s" % (self.base_url, pendding[i])
                        LOG("add %s to download list." % (u,))
                        try:
                            self.pcs.add_task(u, self.pcs_dir)
                        except HTTPError:
                            LOG("add task %s failed" % pendding[i])
            except:
                traceback.print_exc()
            time.sleep(20)

    def upload_finished(self, fn):
        url = "%s/%s" % (self.base_url, fn)
        cur = self.get_cur()
        cur.execute("update video set download_url = '%s',upload_time = '%s' where current_name = '%s'" % (
        url,  time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), fn))
        self.db.conn.commit()
        cur.close()

    def cancel_timeout_task(self, tasks):
        now = time.time()
        timeout = 12 * 60 * 60

        has_timedout = False
        for t in tasks:
            task_id = t["task_id"]
            t1 = int(t["create_time"])
            if (now - t1) > timeout:
                try:
                    LOG("cancel the timeout task %s." % t["task_name"])
                    self.pcs.cancel_task(task_id)
                    has_timedout = True
                except:
                    LOG("cancel task %s failed." % t["task_name"])
                    traceback.print_exc()
        return has_timedout

    def task_exists(self, tasks, fn):
        for t in tasks:
            tn = t["task_name"]
            if t["task_name"] == fn:
                return True
            tn_b,tn_ext = os.path.splitext(tn)
            fn_b,fn_ext = os.path.splitext(fn)
            if tn_b == fn_b:
                return True
        return False

    def print_task_info(self, tasks):
        for t in tasks:
            rr = self.pcs.query_task(t["task_id"])
            r = rr["task_info"][t["task_id"]]
            if int(r["finished_size"]) and int(r["file_size"]):
                LOG("%s %.2f%%(%s/%s) %d" % (
                    t["task_name"],
                    float(r["finished_size"])/float(r["file_size"]) * 100,
                    r["finished_size"],
                    r["file_size"],
                    time.time() - int(t["create_time"])
                    )
                    )

if not mimetypes.inited:
    mimetypes.init()
    EXTENSION_MAPS = mimetypes.types_map.copy()
    for ext in EXTENSIONS:
        e = ext.replace("*",'')
        if not EXTENSION_MAPS.get(e):
            EXTENSION_MAPS[e]=EXT_MAPS.get(e)

class MyLog(object):
    def __init__(self, log_name):
        self.fp = open(log_name, "a")
        self.stdout = sys.stdout

    def write(self, text):
        t = "%s: %s" % (time.asctime(), text)
        self.fp.write(t)
        self.stdout.write(t)
        self.fp.flush()
        self.stdout.flush()

log_fp = None

def LOG(text):
    global log_fp
    if log_fp is not None:
        log_fp.write(text + "\n")

if __name__ == "__main__":

    srv = MovieSrv()

    # redirect console log
    log_fp = MyLog("error.log")
    sys.stdout = log_fp
    sys.stderr = log_fp

    encrypt_instance = spawn(srv.encrypt_file)
    upload_instance = spawn(srv.upload_to_pcs)

    application = srv.application

    log=open("access.log","a", 0)
    port = srv.cfg.getint("server", "port")
    LOG("Listening on port %d." % port)
    WSGIServer(('',port),application, log=log).serve_forever()

